package org.liang.common.utils.zip;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

import net.lingala.zip4j.core.ZipFile;
import net.lingala.zip4j.exception.ZipException;
import net.lingala.zip4j.model.FileHeader;
import net.lingala.zip4j.model.ZipParameters;
import net.lingala.zip4j.util.Zip4jConstants;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.liang.common.utils.io.FileUtil;
import org.liang.common.utils.zip.exception.IncorrectUnzipPasswordException;
import org.liang.common.utils.zip.exception.InvalidOutputPathException;
import org.liang.common.utils.zip.exception.InvalidZipFileException;

/**
 * ZIP 压缩工具类 (支持密码).
 * 
 * @version v1.0 2016年4月13日 上午11:04:27
 */
public class CompressUtil {

    /**
     * 使用给定密码解压指定的ZIP压缩文件到当前目录.
     * 
     * @param zip
     *            指定的ZIP压缩文件
     * @return 解压后文件列表
     * @throws ZipException
     *             压缩文件有损坏或者解压缩失败抛出
     */
    public static List<File> unzip(String zip) {
        File zipfile = new File(zip);
        File parent = zipfile.getParentFile();
        return unzip(zipfile, parent.getAbsolutePath(), null);
    }

    /**
     * 使用给定密码解压指定的ZIP压缩文件到当前目录.
     * 
     * @param zip
     *            指定的ZIP压缩文件
     * @param dest
     *            解压目录
     * @return 解压后文件列表
     * @throws ZipException
     *             压缩文件有损坏或者解压缩失败抛出
     */
    public static List<File> unzip(String zip, String dest) {
        return unzip(new File(zip), dest, null);
    }

    /**
     * 使用给定密码解压指定的ZIP压缩文件到指定目录.
     * 
     * <pre>
     * 如果指定目录不存在,可以自动创建,不合法的路径将导致异常被抛出
     * </pre>
     * 
     * @param zip
     *            指定的ZIP压缩文件
     * @param dest
     *            解压目录
     * @param password
     *            ZIP文件的密码
     * @return 解压后文件列表
     * @throws ZipException
     *             压缩文件有损坏或者解压缩失败抛出
     */
    public static List<File> unzip(String zip, String dest, String password) {
        File zipFile = new File(zip);
        return unzip(zipFile, dest, password);
    }


    /**
     * 使用给定密码解压指定的ZIP压缩文件到指定目录.
     * 
     * <pre>
     * 如果指定目录不存在,可以自动创建。
     * 
     * 需要密码而没有密码会抛出空指针异常{@link NullPointerException}；
     * 错误的解压密码会抛出密码错误异常{@link IncorrectUnzipPasswordException}；
     * 不合法的路径将会抛出无效的输出路径异常 {@link InvalidOutputPathException}；
     * 
     * ZIP 压缩过程中其他异常 则会根据程序实际情况进行抛出，
     * 具体异常内容请查看zip4j（此部分异常基本不会被抛出）。
     * </pre>
     * 
     * @param zipfile
     *            指定的ZIP压缩文件
     * @param dest
     *            解压目录
     * @param password
     *            ZIP文件的密码
     * @return 解压后文件数组
     */
    private static List<File> unzip(File zipfile, String dest, String password) {
        try {
            return extract(zipfile, dest, password);
        } catch (ZipException e) {
            if (e.getMessage().contains("Wrong Password")) {
                throw new IncorrectUnzipPasswordException();
            } else if (e.getMessage().contains("output path")
                       && e.getMessage().contains("invalid")) {
                throw new InvalidOutputPathException();
            } else {
                throw new RuntimeException(e);
            }
        }
    }

    /**
     * 提取压缩文件中的文件.
     * 
     * @param zip
     *            指定的ZIP压缩文件
     * @param dest
     *            解压目录
     * @param password
     *            ZIP文件的密码
     * @return 解压后文件数组
     * @throws ZipException
     *             压缩过程中抛出的异常
     */
    private static List<File> extract(File zipFile, String dest, String password)
                    throws ZipException {
        ZipFile zFile = new ZipFile(zipFile);
        zFile.setFileNameCharset("UTF-8");

        if (!zFile.isValidZipFile()) {
            throw new InvalidZipFileException();
        }

        File destDir = new File(dest);
        if (destDir.isDirectory() && !destDir.exists()) {
            destDir.mkdir();
        }

        if (zFile.isEncrypted()) {
            zFile.setPassword(password.toCharArray());
        }

        zFile.extractAll(dest);

        @SuppressWarnings("unchecked")
        List<FileHeader> headerList = zFile.getFileHeaders();

        List<File> extractedFiles = new ArrayList<File>();
        for (FileHeader fileHeader : headerList) {
            if (!fileHeader.isDirectory()) {
                extractedFiles.add(new File(destDir, fileHeader.getFileName()));
            }
        }
        return extractedFiles;
    }

    /**
     * 压缩指定文件到当前文件夹.
     * 
     * @param src
     *            要压缩的指定文件
     * @return 最终的压缩文件存放的绝对路径.
     */
    public static String zip(String src) {
        return zip(src, null);
    }

    /**
     * 使用给定密码压缩指定文件或文件夹到当前目录.
     * 
     * @param src
     *            要压缩的文件
     * @param dest
     *            压缩文件存放路径
     * @return 最终的压缩文件存放的绝对路径.
     */
    public static String zip(String src, String dest) {
        return zip(src, dest, null);
    }

    /**
     * 使用给定密码压缩指定文件或文件夹到指定位置.
     * 
     * <pre>
     * 以源文件/文件夹 为zip名称；
     * 以目标文件/文件夹为zip父目录
     * 如果dest为文件 则 使用zip目录为dest文件同目录下
     * eg.
     * src: /opt/source.file, dest: /usr/target.zip --> /usr/target.zip
     * src: /opt/source, dest: /usr/target.zip --> /usr/target.zip
     * src: /opt/source, dest: /usr/target.file --> /usr/source.zip
     * src: /opt/source.file, dest: /usr/target.file --> /usr/source.zip
     * 
     * src: /opt/source.file, dest: null --> /opt/source.zip
     * src: /opt/source.file, dest: /opt/zip --> /opt/zip/source.zip
     * 
     * src: /opt/source, dest: null --> /opt/source.zip
     * src: /opt/source, dest: /opt/zip --> /opt/zip/source.zip
     * src: /opt/folder/source, dest: /opt --> /opt/source.zip
     * src: /opt/source.file, dest: /opt --> /opt/source.zip
     * </pre>
     * 
     * @param src
     *            要压缩的文件或文件夹路径
     * @param dest
     *            压缩文件存放路径 （文件/文件夹/null）
     * @param password
     *            压缩使用的密码
     * @return 最终的压缩文件存放的绝对路径
     */
    public static String zip(String src, String dest, String password) {
        try {
            ZipParameters parameters = new ZipParameters();
            parameters.setCompressionMethod(Zip4jConstants.COMP_DEFLATE); // 压缩方式
            parameters.setCompressionLevel(Zip4jConstants.DEFLATE_LEVEL_NORMAL); // 压缩级别
            parameters.setIncludeRootFolder(false);
            encrypt(password, parameters);

            File srcfile = new File(src);

            dest = generateZippath(srcfile, dest);
            ZipFile zipfile = new ZipFile(dest);

            if (srcfile.isDirectory()) {
                zipfile.addFolder(srcfile, parameters);
            } else {
                zipfile.addFile(srcfile, parameters);
            }

            return dest;
        } catch (ZipException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 设置zip密码.
     * 
     * <pre>
     * 如果密码不为空则进行加密。
     * </pre>
     * 
     * @param password
     *            密码
     * @param parameters
     *            zip参数
     */
    private static void encrypt(String password, ZipParameters parameters) {
        if (StringUtils.isNotEmpty(password)) {
            parameters.setEncryptFiles(true);
            parameters.setEncryptionMethod(Zip4jConstants.ENC_METHOD_STANDARD); // 加密方式
            parameters.setPassword(password.toCharArray());
        }
    }

    /**
     * 构建压缩文件存放路径.
     * 
     * <pre>
     * 以源文件/文件夹 为zip名称；
     * 以目标文件/文件夹为zip父目录
     * 如果dest为文件 则 使用zip目录为dest文件同目录下
     * </pre>
     * 
     * @param srcfile
     *            源文件（文件/文件夹）
     * @param dest
     *            压缩目标路径 （文件/文件夹/null）
     * @return 正确的压缩文件存放路径
     */
    static String generateZippath(File srcfile, String dest) {
        if (isZipfile(dest)) {
            FileUtil.createDir(FilenameUtils.getFullPath(dest));
            return dest;
        }
        String target = StringUtils.isEmpty(dest)
                        ? srcfile.getParent() : getTargetPath(dest);
        FileUtil.createDir(target);

        return (target + File.separator + getFilename(srcfile) + ".zip");
    }

    private static boolean isZipfile(String dest) {
        String extension = FilenameUtils.getExtension(dest);
        return StringUtils.isNotEmpty(extension) && extension.equals("zip");
    }

    private static String getTargetPath(String dest) {
        String extension = FilenameUtils.getExtension(dest);
        return StringUtils.isNotEmpty(extension) ? FilenameUtils.getFullPath(dest) : dest;
    }

    /**
     * 获取文件名（不包含后缀）.
     * 
     * <pre>
     * 如果传入文件时文件夹则返回文件夹名称，
     * 如果是文件则返回文件名称。
     * 
     * a/b/c.txt --> c
     * a.txt     --> a
     * a/b/c     --> c
     * a/b/c/    --> （不会出现）
     * </pre>
     * 
     * @param srcfile
     *            源文件
     * @return 文件名称（不包含后缀名）
     */
    private static String getFilename(File srcfile) {
        String extension = FilenameUtils.getExtension(srcfile.getName());
        return StringUtils.isNotEmpty(extension)
                        ? FilenameUtils.getBaseName(srcfile.getName()) : srcfile.getName();
    }
}